from twisted.words.xish import domish, xpath
from twisted.words.protocols.jabber import xmlstream
from twisted.python import log
from twisted.internet import threads, defer, reactor
from wokkel.subprotocols import XMPPHandler
from datetime import datetime
from lxml import etree
import time, os, subprocess

import config as commonConfig

class TriggerException( Exception ): pass

class TriggerHandler( XMPPHandler ):
    scheduled_triggers = []
    timed_triggers = []
    event_triggers = []
    xpath_triggers = []
    triggering_events = []
    
    def __init__(self, parent, trigger_config):
        XMPPHandler.__init__( self )
        
        self.my_parent = parent        
        config = commonConfig.Config( trigger_config, 'triggers' )
        
        scheduled  = config.getSection( 'scheduled' )
        timed      = config.getSection( 'timed' )
        event      = config.getSection( 'events' )
        xpath      = config.getSection( 'xpath' )
        
        for trigger in scheduled:
            self.scheduled_triggers.append( ScheduledTrigger( self, trigger ) )
        
        for trigger in timed:
            self.timed_triggers.append( TimedTrigger( self, trigger ) )
        
        for trigger in event:
            self.event_triggers.append( EventTrigger( self, trigger ) )
        
        for trigger in xpath:
            self.xpath_triggers.append( XpathTrigger( self, trigger ) )

        if len( self.scheduled_triggers ):
            reactor.callLater( 60, self.checkScheduled )
            
        if len( self.timed_triggers ):
            reactor.callLater( 60, self.checkTimed )
            
    def connectionInitialized(self):
        log.msg( 'trigger_handler: connectionInitialized' )        
        for trigger in self.event_triggers:
            self.xmlstream.addObserver('/' + trigger.event.type, self.checkEvent, trigger = trigger )
            
        #for trigger in self.xpath_triggers:
        if len( self.xpath_triggers ):
            self.xmlstream.addObserver( '/*', self.checkXpathEvent )
               
    def checkEvent(self, element, trigger):
        if not len( self.event_triggers ):
            return
        
        def checkResponse( response ):
            log.msg( 'checkResponse' )
            trigger.check_running = False
            if response:
                trigger.run()
        
        if not trigger.check_running:
            trigger.check_running = True
            trigger.check( element ).addCallback( checkResponse )    
        
    def checkXpathEvent(self, element):
        if not len( self.xpath_triggers ):
            return
        
        def checkResponse( response, trigger ):
            log.msg( 'checkResponse: %s' % trigger.name )
            trigger.check_running = False
            print response
            if response:
                trigger.run()
        
        for trigger in self.xpath_triggers:
            if not trigger.check_running:
                trigger.check_running = True
                trigger.check( element ).addCallback( checkResponse, trigger )
        
    def checkScheduled(self):
        
        def checkResponse( response, trigger ):
            log.msg( 'checkResponse' )
            trigger.check_running = False
            if response:
                trigger.run()  
        
        for trigger in self.scheduled_triggers:
            if not trigger.check_running:
                trigger.check_running = True
                trigger.check().addCallback( checkResponse, trigger )
            
        reactor.callLater( 60, self.checkScheduled )
                
    def checkTimed(self):   
                
        def checkResponse( response, trigger ):
            log.msg( 'checkResponse' )
            trigger.check_running = False
            if response:
                trigger.run()  
                
        for trigger in self.timed_triggers:
            if not trigger.check_running:
                trigger.check_running = True
                trigger.check().addCallback( checkResponse, trigger )
            
        reactor.callLater( 60, self.checkTimed )      
    
    def addXpathTrigger(self, trigger):
        if not isinstance( trigger, XpathTrigger ):
            raise TriggerException( 'trigger is not an XpathTrigger' )
        
        trigger.parent = self
        self.xpath_triggers.append( trigger )
        
        return True
        
def MessageAction( trigger ):
    target_entity = trigger.action['target_entity']
    message_type = trigger.action['message_type']
    
    content = ParseTriggerContent( trigger.action['content'] )
    
    def getContentResponse( response ):
        log.msg( 'getContentResponse' )
        msg = domish.Element( (None, 'message') )
        msg['type'] = message_type
        msg['to'] = target_entity
            
        msg.addElement('body', None, response )
        trigger.parent.xmlstream.send( msg )
            
    content.getContent().addCallback( getContentResponse )
    
def FunctionAction( trigger ):
    target_function = trigger.action['function']
    
    if 'args' in trigger.action:
        target_function( trigger, *trigger.action['args'] )
    else:
        target_function( trigger )

    
def ParseTriggerContent( config ):
    type = config['type']
    
    if not type in TriggerContent.content_types:
        return False
    
    trigger_class = eval( '%sTriggerContent' % type.capitalize() )
    
    return trigger_class( config )
    
def ParseEvent( config ):
    type = config['type']
    
    if not type in EventType.event_types:
        return False
    
    event_class = eval( '%sEventType' % type.capitalize() )
    
    return event_class( config )
    
class Trigger( object ):
    action_types = { 'message': MessageAction }
    ran = False
    check_running = False
    
    def __init__(self, parent, config):

        self.parent = parent
        
        self.config = config
        self.name = config['name']
        self.repeat = config.get( 'repeat', False )
        self.action = config['action']
    
        if not self.action['type'] in self.action_types:
            raise TriggerException( 'invalid trigger type' )
        
        self._action = self.action_types[ self.action['type'] ]
        
    def check(self):
        pass
    
    def run(self):
        pass
        
class ScheduledTrigger( Trigger ):
    
    def __init__(self, parent, config):
        Trigger.__init__(self, parent, config)
        
        if not len( config['trigger_time'] ):
            raise TriggerException( 'invalid trigger time' )
        
        self.tt_year = config['trigger_time'].get( 'year', False )
        self.tt_mon  = config['trigger_time'].get( 'month', False )
        self.tt_day  = config['trigger_time'].get( 'day', False )
        
        self.tt_hour = config['trigger_time'].get( 'hour', False )
        self.tt_min  = config['trigger_time'].get( 'min', False )
        
    def check(self):
        if self.ran and not self.repeat:
            return defer.succeed( False )
        
        if not ( self.tt_year or self.tt_mon ):
            return defer.succeed( False ) 
 
        time = datetime.now()
        
        year_match, mon_match, day_match, hour_match, min_match = [ True ] * 5  
        
        if self.tt_year:
            year_match = ( self.tt_year == time.year and True ) or False 
        
        if self.tt_mon:
            mon_match = ( self.tt_mon == time.mon and True ) or False
            
        if self.tt_day:
            day_match = ( self.tt_day == time.day and True ) or False
            
        if self.tt_hour:
            hour_match = ( self.tt_hour == time.hour and True ) or False
        
        if self.tt_min:
            min_match = ( self.tt_min == time.min and True ) or False
            
        if [ year_match, mon_match, day_match, hour_match, min_match ] == [ True ] * 5:
            return defer.succeed( True )
        
        return defer.succeed( False )
    
    def run(self):
        self.ran = True
        return self._action( self )
                                    
class TimedTrigger( Trigger ):
    
    def __init__(self, parent, config):
        Trigger.__init__(self, parent, config)
        
        if not len( config['trigger_delay'] ):
            raise TriggerException( 'invalid trigger delay' )
        
        self.start = time.time()
        self.last_run = self.start
        self.delay = float( config['trigger_delay'] )
        
    def check(self):
        log.msg( 'TimedTrigger: check' )
        if self.ran and not self.repeat:
            return defer.succeed( False )
        
        if ( time.time() - self.last_run ) > self.delay:
            return defer.succeed( True )
        
        return defer.succeed( False )
        
    def run(self):
        log.msg( 'TimedTrigger: run' )
        self.ran = True
        self.last_run = time.time()
        return self._action( self )
    
class EventTrigger( Trigger ):
    event_types = [ 'message', 'presence', 'iq' ]
    
    def __init__(self, parent, config):
        Trigger.__init__(self, parent, config)
        
        if not len( config['event'] ):
            raise TriggerException( 'invalid trigger event' )
        
        self.event = ParseEvent( config['event'] )

    def check(self, element):
        if self.ran and not self.repeat:
            return defer.succeed( False )
        
        def matchElementResponse( response ):
            log.msg( 'matchElementResponse' )
            if not response:
                return False
            
            return True
        
        return self.event.matchElement( element ).addCallback( matchElementResponse )
    
    def run(self):
        self.ran = True
        return self._action( self )
    
class XpathTrigger( Trigger ):
    action_types = { 'message': MessageAction, 'function': FunctionAction }
    
    def __init__(self, parent, config):
        Trigger.__init__(self, parent, config)
        
        self.xpath = unicode( config['xpath'] )
        self.xquery = etree.ETXPath( config['xpath'] )

        self.content = False
        
        if 'content' in config:
            self.content = ParseTriggerContent( config['content'] )
            
    def check(self, element):
        if self.ran and not self.repeat:
            return defer.succeed( False )
        
        if not self.xquery( etree.XML( element.toXml() ) ):
            return defer.succeed( False )       
        
        """
        def getContentResponse( response ):
            log.msg( 'getContentResponse' )
            if response == str( element.body ):
                return True
            
            return False
        
        if self.content:
            return self.content.getContent().addCallback( getContentResponse )
        """
        
        return defer.succeed( True ) 
 
    def run(self):
        self.ran = True
        return self._action( self )
                                               
class TriggerContent( object ):
    content_types = [ 'text', 'file', 'external' ]
    
    def __init__(self, config):
        self.type = config['type']
        
        if not self.type in self.content_types:
            raise TriggerException( 'invalid content type' )
        
    def getContent(self):
        pass
    
class TextTriggerContent( TriggerContent ):
    
    def __init__(self, config):
        TriggerContent.__init__(self, config)
        
        self.value = config['value']
        
    def getContent(self):
        return defer.succeed( self.value ) 
        
class FileTriggerContent( TriggerContent ):
    
    def __init__(self, config):
        TriggerContent.__init__(self, config)
        
        self.file = config['file_path']
        
    def getContent(self):
        if not os.path.exists( self.file ) or not os.access( self.file, os.R_OK ):
            return 'file not found'
        
        fd = open( self.file )
        fd_raw = fd.read()
        fd.close()
        
        return defer.succeed( str( fd_raw ) )
    
class ExternalTriggerContent( TriggerContent ):
    
    def __init__(self, config):
        TriggerContent.__init__(self, config)
        
        self.exec_path = config['exec_path']
        self.timeout = 300
        
        if 'timeout' in config:
            self.timeout = int( config['timeout'] )
        
    def getContent(self):
        if not os.path.exists( self.exec_path ) or not os.access( self.exec_path, os.X_OK ):
            return defer.succeed( 'command not accessible' )
        
        proc = subprocess.Popen( [ os.path.abspath( self.exec_path ) ], stdout = subprocess.PIPE, stderr = subprocess.PIPE )

        def getExternalContentCallback( response ):
            log.msg( 'getExternalContentCallback' )
            if response:
                log.err( 'External process returned abnormally' )
                log.err( proc.stderr.read() )
            
            return str( proc.stdout.read() )
        
        return threads.deferToThread( proc.wait ).addCallback( getExternalContentCallback ).addErrback( log.err )
    
class EventType( object ):
    event_types = [ 'message', 'xpath' ]
    
    def __init__(self, config):
        self.type = config['type']
        
        if not self.type in self.event_types:
            raise TriggerException( 'invalid event type' )
        
    def matchElement(self, element):
        pass
        
class MessageEventType( EventType ):
    
    def __init__(self, config):
        EventType.__init__(self, config)
        
        self.source_entity = config['source_entity']
        self.content = False
        
        if 'content' in config:
            self.content = ParseTriggerContent( config['content'] )
        
    def matchElement(self, element):
        if element.name != self.type:
            return defer.succeed( False )
        
        if not element['from'] == self.source_entity:
            return defer.succeed( False )
        
        def getContentResponse( response ):
            log.msg( 'getContentResponse' )
            if response == str( element.body ):
                return True
            
            return False
        
        if self.content:
            return self.content.getContent().addCallback( getContentResponse ) 
        
        return defer.succeed( True )
